Skip to content

Adds TimelockGuard#17465

Closed
maurelian wants to merge 58 commits intodevelopfrom
jm/timelock-nonnested
Closed

Adds TimelockGuard#17465
maurelian wants to merge 58 commits intodevelopfrom
jm/timelock-nonnested

Conversation

@maurelian
Copy link
Copy Markdown
Contributor

Description

Adds a TimelockGuard contract, which is a Safe Guard to enable schedule and cancellation of transactions.

For a full spec, see ethereum-optimism/specs#761.

@codecov
Copy link
Copy Markdown

codecov bot commented Sep 17, 2025

Codecov Report

❌ Patch coverage is 95.28302% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.87%. Comparing base (fd5f8e3) to head (5efe9a6).
⚠️ Report is 90 commits behind head on develop.

Files with missing lines Patch % Lines
...kages/contracts-bedrock/src/safe/TimelockGuard.sol 95.28% 5 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           develop   #17465       +/-   ##
============================================
+ Coverage    81.88%   95.87%   +13.98%     
============================================
  Files          168      114       -54     
  Lines         9959     5280     -4679     
============================================
- Hits          8155     5062     -3093     
+ Misses        1658      218     -1440     
+ Partials       146        0      -146     
Flag Coverage Δ
cannon-go-tests-64 ?
contracts-bedrock-tests 95.87% <95.28%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...kages/contracts-bedrock/src/safe/TimelockGuard.sol 95.28% <95.28%> (ø)

... and 55 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

}

/// @notice Mapping from Safe address to its guard configuration
mapping(Safe => GuardConfig) public safeConfigs;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timelockSafeConfiguration, as per the specs. So that it can be merged with LivenessModule.

/// @dev MUST never revert
/// @param _safe The Safe address to query
/// @return The timelock delay in seconds
function viewTimelockGuardConfiguration(Safe _safe) public view returns (GuardConfig memory) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed, we can just query timelockSafeConfiguration

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I had trouble getting solidity to auto generate the getter with just mapping(Safe => Config) public timelockGuardSafeConfig; which surprised me. I'll leave this open so that I can test that out again.

/// @notice Returns the list of all scheduled but not cancelled transactions for a given safe
/// @dev MUST NOT revert - NOT IMPLEMENTED YET
/// @return List of pending transaction hashes
function checkPendingTransactions(address) external pure returns (bytes32[] memory) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function checkPendingTransactions(address) external pure returns (bytes32[] memory) {
function getAllScheduledTransactions(address) external pure returns (bytes32[] memory) {

Or we can have getScheduledTransaction and getScheduledTransactions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I will assume that the full interface for those two functions is:

getScheduledTransaction(Safe _safe, bytes32 _txHash) returns (ScheduledTransaction);
getScheduledTransactions(Safe _safe) returns (ScheduledTransaction[]);

/// @dev MUST NOT revert - NOT IMPLEMENTED YET
/// @return List of pending transaction hashes
function checkPendingTransactions(address) external pure returns (bytes32[] memory) {
return new bytes32[](0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder how to actually code this, since we don't remove scheduled transactions from storage.

We might need to keep an additional enumerable set or dynamic array where we add scheduled transactions (duplicating scheduledTransactions) but where they are removed once executed or cancelled.

Or we have 3 separate enumerable sets and we move transactions between them:
scheduledTransactions
cancelledTransactions
executedTransactions

It all feels quite messy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on a separate conversation that keeping the scheduled but not executed or cancelled transactions in a EnumerableSet that gets a duplicate of the relevant contents in scheduledTransactions is the safest and cleanest option if we need this function.

@maurelian maurelian force-pushed the jm/timelock-nonnested branch from 3afbab9 to 2791608 Compare September 19, 2025 13:46
Copy link
Copy Markdown
Contributor Author

maurelian commented Sep 19, 2025

This stack of pull requests is managed by Graphite. Learn more about stacking.

maurelian and others added 11 commits September 19, 2025 16:04
- Add configureTimelockGuard function to allow Safes to set timelock delays
- Validate timelock delay between 1 second and 1 year
- Check that guard is properly enabled on calling Safe using getStorageAt()
- Store configuration per Safe with GuardConfigured event emission
- Add comprehensive test suite covering all spec requirements
- Implement IGuard interface for Safe compatibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add clearTimelockGuard function to allow Safes to clear timelock configuration
- Validate that guard is disabled before allowing configuration clearing
- Check that Safe was previously configured before clearing
- Delete configuration data and emit GuardCleared event
- Add comprehensive test suite covering all spec requirements
- Add new errors: TimelockGuard_GuardNotConfigured, TimelockGuard_GuardStillEnabled

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add internal _getGuard() helper to centralize guard address retrieval
- Update configureTimelockGuard and clearTimelockGuard to use helper
- Reduces code duplication and improves maintainability

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add cancellationThreshold function to return current threshold for a Safe
- Return 0 if guard not enabled or not configured
- Initialize to 1 when Safe configures guard
- Clear threshold when Safe clears guard configuration
- Add comprehensive test suite covering all spec requirements
- Function never reverts as per spec requirements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
…lity

- Add scheduleTransaction placeholder (Function 4)
- Add checkPendingTransactions placeholder (Function 6)
- Add rejectTransaction placeholder (Function 7)
- Add rejectTransactionWithSignature placeholder (Function 8)
- Add cancelTransaction placeholder (Function 9)
- Update checkTransaction placeholder (Function 5)
- All placeholders have proper signatures and documentation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add CancellationThresholdUpdated event and emit it from threshold
modification functions. Remove threshold parameter from TransactionCancelled
event for cleaner separation of concerns.
We can simply use `timelock > 0` as an indicator of configuration
The right way to do this is now just to set timelockDelay to zero.
This library significantly reduces the amount of boilerplace required to
setup a Safe transaction, then schedule, cancel, or execute it.
@maurelian maurelian force-pushed the jm/timelock-nonnested branch from 8f252c5 to 581cab0 Compare September 19, 2025 20:05
Comment on lines +806 to +808
// TODO: this test fails because the guard config cannot be modified while the guard is
// disabled. IMO a guard should be able to manage its own configuration while it is disabled.
vm.skip(true);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uint256 blockingThreshold = _blockingThreshold(_safe);
uint256 quorum = _safe.getThreshold();
// Return the minimum of the blocking threshold and the quorum
return (blockingThreshold < quorum ? blockingThreshold : quorum) - 1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return (blockingThreshold < quorum ? blockingThreshold : quorum) - 1;
return (blockingThreshold < quorum ? blockingThreshold : quorum);

The max_cancellation_threshold is min(blocking_threshold, quorum)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants